@nuvia-ui/components 4.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.
- package/package.json +27 -0
- package/src/ds-accordion/ds-accordion-item.js +288 -0
- package/src/ds-accordion/ds-accordion-item.stories.js +82 -0
- package/src/ds-accordion/ds-accordion.a11y.test.js +92 -0
- package/src/ds-accordion/ds-accordion.js +68 -0
- package/src/ds-accordion/ds-accordion.stories.js +118 -0
- package/src/ds-accordion/ds-accordion.test.js +146 -0
- package/src/ds-accordion/index.js +2 -0
- package/src/ds-action-bar/ds-action-bar.js +116 -0
- package/src/ds-action-bar/ds-action-bar.stories.js +86 -0
- package/src/ds-action-bar/ds-action-bar.test.js +64 -0
- package/src/ds-action-bar/index.js +1 -0
- package/src/ds-alert/ds-alert.a11y.test.js +151 -0
- package/src/ds-alert/ds-alert.js +223 -0
- package/src/ds-alert/ds-alert.mdx +142 -0
- package/src/ds-alert/ds-alert.stories.js +166 -0
- package/src/ds-alert/ds-alert.test.js +256 -0
- package/src/ds-alert/index.js +1 -0
- package/src/ds-avatar/ds-avatar.a11y.test.js +45 -0
- package/src/ds-avatar/ds-avatar.js +216 -0
- package/src/ds-avatar/ds-avatar.stories.js +120 -0
- package/src/ds-avatar/ds-avatar.test.js +83 -0
- package/src/ds-avatar/index.js +1 -0
- package/src/ds-avatar-extended/ds-avatar-extended.a11y.test.js +29 -0
- package/src/ds-avatar-extended/ds-avatar-extended.js +108 -0
- package/src/ds-avatar-extended/ds-avatar-extended.stories.js +93 -0
- package/src/ds-avatar-extended/ds-avatar-extended.test.js +66 -0
- package/src/ds-avatar-extended/index.js +1 -0
- package/src/ds-banner/ds-banner.a11y.test.js +51 -0
- package/src/ds-banner/ds-banner.js +233 -0
- package/src/ds-banner/ds-banner.stories.js +185 -0
- package/src/ds-banner/ds-banner.test.js +116 -0
- package/src/ds-banner/index.js +1 -0
- package/src/ds-breadcrumb-item/ds-breadcrumb-item.js +135 -0
- package/src/ds-breadcrumb-item/ds-breadcrumb-item.stories.js +49 -0
- package/src/ds-breadcrumb-item/ds-breadcrumb-item.test.js +55 -0
- package/src/ds-breadcrumbs/ds-breadcrumbs.js +194 -0
- package/src/ds-breadcrumbs/ds-breadcrumbs.stories.js +54 -0
- package/src/ds-breadcrumbs/ds-breadcrumbs.test.js +33 -0
- package/src/ds-button/ds-button.a11y.test.js +49 -0
- package/src/ds-button/ds-button.js +205 -0
- package/src/ds-button/ds-button.mdx +141 -0
- package/src/ds-button/ds-button.stories.js +152 -0
- package/src/ds-button/ds-button.test.js +62 -0
- package/src/ds-button/index.js +1 -0
- package/src/ds-button-group/ds-button-group.js +82 -0
- package/src/ds-button-group/ds-button-group.mdx +39 -0
- package/src/ds-button-group/ds-button-group.stories.js +47 -0
- package/src/ds-button-group/ds-button-group.test.js +47 -0
- package/src/ds-button-group/index.js +1 -0
- package/src/ds-checkbox/ds-checkbox.a11y.test.js +79 -0
- package/src/ds-checkbox/ds-checkbox.js +271 -0
- package/src/ds-checkbox/ds-checkbox.stories.js +77 -0
- package/src/ds-checkbox/ds-checkbox.test.js +191 -0
- package/src/ds-checkbox/index.js +1 -0
- package/src/ds-checkbox-group/ds-checkbox-group.a11y.test.js +146 -0
- package/src/ds-checkbox-group/ds-checkbox-group.js +235 -0
- package/src/ds-checkbox-group/ds-checkbox-group.stories.js +210 -0
- package/src/ds-checkbox-group/ds-checkbox-group.test.js +150 -0
- package/src/ds-checkbox-group/index.js +1 -0
- package/src/ds-dialog/ds-dialog.js +466 -0
- package/src/ds-dialog/ds-dialog.stories.js +274 -0
- package/src/ds-dialog/ds-dialog.test.js +441 -0
- package/src/ds-dialog/index.js +1 -0
- package/src/ds-dropdown/ds-dropdown.a11y.test.js +80 -0
- package/src/ds-dropdown/ds-dropdown.js +891 -0
- package/src/ds-dropdown/ds-dropdown.stories.js +259 -0
- package/src/ds-dropdown/ds-dropdown.test.js +268 -0
- package/src/ds-dropdown/index.js +1 -0
- package/src/ds-dropdown-group/ds-dropdown-group.js +55 -0
- package/src/ds-dropdown-panel/ds-dropdown-panel.js +34 -0
- package/src/ds-file-uploaded/ds-file-uploaded.a11y.test.js +40 -0
- package/src/ds-file-uploaded/ds-file-uploaded.js +135 -0
- package/src/ds-file-uploaded/ds-file-uploaded.mdx +33 -0
- package/src/ds-file-uploaded/ds-file-uploaded.stories.js +81 -0
- package/src/ds-file-uploaded/ds-file-uploaded.test.js +85 -0
- package/src/ds-file-uploader/ds-file-uploader.a11y.test.js +61 -0
- package/src/ds-file-uploader/ds-file-uploader.js +442 -0
- package/src/ds-file-uploader/ds-file-uploader.mdx +44 -0
- package/src/ds-file-uploader/ds-file-uploader.stories.js +76 -0
- package/src/ds-file-uploader/ds-file-uploader.test.js +142 -0
- package/src/ds-header/ds-header.a11y.test.js +38 -0
- package/src/ds-header/ds-header.js +149 -0
- package/src/ds-header/ds-header.stories.js +63 -0
- package/src/ds-header/ds-header.test.js +52 -0
- package/src/ds-header/index.js +1 -0
- package/src/ds-header-nav/ds-header-nav.a11y.test.js +69 -0
- package/src/ds-header-nav/ds-header-nav.js +114 -0
- package/src/ds-header-nav/ds-header-nav.stories.js +17 -0
- package/src/ds-header-nav/ds-header-nav.test.js +93 -0
- package/src/ds-header-nav-item/ds-header-nav-item.a11y.test.js +71 -0
- package/src/ds-header-nav-item/ds-header-nav-item.js +124 -0
- package/src/ds-header-nav-item/ds-header-nav-item.stories.js +43 -0
- package/src/ds-header-nav-item/ds-header-nav-item.test.js +61 -0
- package/src/ds-icon/ds-icon.a11y.test.js +49 -0
- package/src/ds-icon/ds-icon.js +75 -0
- package/src/ds-icon/ds-icon.mdx +36 -0
- package/src/ds-icon/ds-icon.stories.js +88 -0
- package/src/ds-icon/ds-icon.test.js +97 -0
- package/src/ds-icon/index.js +1 -0
- package/src/ds-icon-button/ds-icon-button.a11y.test.js +55 -0
- package/src/ds-icon-button/ds-icon-button.js +224 -0
- package/src/ds-icon-button/ds-icon-button.mdx +131 -0
- package/src/ds-icon-button/ds-icon-button.stories.js +128 -0
- package/src/ds-icon-button/ds-icon-button.test.js +90 -0
- package/src/ds-icon-button/index.js +1 -0
- package/src/ds-input/ds-input.a11y.test.js +145 -0
- package/src/ds-input/ds-input.js +645 -0
- package/src/ds-input/ds-input.mdx +251 -0
- package/src/ds-input/ds-input.stories.js +298 -0
- package/src/ds-input/ds-input.test.js +792 -0
- package/src/ds-input/index.js +1 -0
- package/src/ds-link/ds-link.js +111 -0
- package/src/ds-link/ds-link.stories.js +56 -0
- package/src/ds-link/ds-link.test.js +74 -0
- package/src/ds-list-item/ds-list-item.a11y.test.js +39 -0
- package/src/ds-list-item/ds-list-item.js +292 -0
- package/src/ds-list-item/ds-list-item.stories.js +101 -0
- package/src/ds-list-item/ds-list-item.test.js +63 -0
- package/src/ds-menu/ds-menu.js +30 -0
- package/src/ds-menu/ds-menu.stories.js +120 -0
- package/src/ds-menu/ds-menu.test.js +123 -0
- package/src/ds-menu-group/ds-menu-group.js +101 -0
- package/src/ds-menu-group/ds-menu-group.stories.js +99 -0
- package/src/ds-nav-item/ds-nav-item.a11y.test.js +91 -0
- package/src/ds-nav-item/ds-nav-item.js +307 -0
- package/src/ds-nav-item/ds-nav-item.stories.js +99 -0
- package/src/ds-nav-item/ds-nav-item.test.js +169 -0
- package/src/ds-nav-item/index.js +1 -0
- package/src/ds-nav-vertical/ds-nav-vertical.a11y.test.js +69 -0
- package/src/ds-nav-vertical/ds-nav-vertical.js +173 -0
- package/src/ds-nav-vertical/ds-nav-vertical.stories.js +124 -0
- package/src/ds-nav-vertical/ds-nav-vertical.test.js +176 -0
- package/src/ds-nav-vertical/index.js +1 -0
- package/src/ds-pagination/ds-pagination.a11y.test.js +50 -0
- package/src/ds-pagination/ds-pagination.js +232 -0
- package/src/ds-pagination/ds-pagination.stories.js +63 -0
- package/src/ds-pagination/ds-pagination.test.js +141 -0
- package/src/ds-pagination/index.js +1 -0
- package/src/ds-progress-bar/ds-progress-bar.a11y.test.js +25 -0
- package/src/ds-progress-bar/ds-progress-bar.js +81 -0
- package/src/ds-progress-bar/ds-progress-bar.stories.js +69 -0
- package/src/ds-progress-bar/ds-progress-bar.test.js +60 -0
- package/src/ds-radio/ds-radio.a11y.test.js +69 -0
- package/src/ds-radio/ds-radio.js +240 -0
- package/src/ds-radio/ds-radio.stories.js +102 -0
- package/src/ds-radio/ds-radio.test.js +114 -0
- package/src/ds-radio/index.js +1 -0
- package/src/ds-radio-group/ds-radio-group.a11y.test.js +164 -0
- package/src/ds-radio-group/ds-radio-group.js +257 -0
- package/src/ds-radio-group/ds-radio-group.stories.js +247 -0
- package/src/ds-radio-group/ds-radio-group.test.js +194 -0
- package/src/ds-radio-group/index.js +1 -0
- package/src/ds-rich-list/ds-rich-list.js +246 -0
- package/src/ds-rich-list/ds-rich-list.stories.js +368 -0
- package/src/ds-rich-list/ds-rich-list.test.js +293 -0
- package/src/ds-rich-list-item/ds-rich-list-item.js +579 -0
- package/src/ds-rich-list-item/ds-rich-list-item.stories.js +197 -0
- package/src/ds-rich-list-item/ds-rich-list-item.test.js +434 -0
- package/src/ds-slider/ds-slider.js +399 -0
- package/src/ds-slider/ds-slider.stories.js +107 -0
- package/src/ds-slider/ds-slider.test.js +308 -0
- package/src/ds-spinner/ds-spinner.js +173 -0
- package/src/ds-spinner/ds-spinner.stories.js +52 -0
- package/src/ds-spinner/ds-spinner.test.js +50 -0
- package/src/ds-status-border/ds-status-border.js +88 -0
- package/src/ds-status-border/ds-status-border.stories.js +242 -0
- package/src/ds-status-border/ds-status-border.test.js +168 -0
- package/src/ds-stepper/ds-stepper.a11y.test.js +198 -0
- package/src/ds-stepper/ds-stepper.js +207 -0
- package/src/ds-stepper/ds-stepper.stories.js +530 -0
- package/src/ds-stepper/ds-stepper.test.js +311 -0
- package/src/ds-stepper-item/ds-stepper-item.js +485 -0
- package/src/ds-stepper-item/ds-stepper-item.stories.js +288 -0
- package/src/ds-switch/ds-switch.js +348 -0
- package/src/ds-switch/ds-switch.stories.js +145 -0
- package/src/ds-switch/ds-switch.test.js +226 -0
- package/src/ds-switch/index.js +1 -0
- package/src/ds-tab-item/ds-tab-item.js +341 -0
- package/src/ds-tab-item/ds-tab-item.stories.js +69 -0
- package/src/ds-tabs/ds-tab-panel.js +48 -0
- package/src/ds-tabs/ds-tabs.a11y.test.js +56 -0
- package/src/ds-tabs/ds-tabs.js +180 -0
- package/src/ds-tabs/ds-tabs.stories.js +152 -0
- package/src/ds-tabs/ds-tabs.test.js +306 -0
- package/src/ds-tabs/index.js +3 -0
- package/src/ds-tag-action/ds-tag-action.a11y.test.js +32 -0
- package/src/ds-tag-action/ds-tag-action.js +185 -0
- package/src/ds-tag-action/ds-tag-action.stories.js +55 -0
- package/src/ds-tag-action/ds-tag-action.test.js +44 -0
- package/src/ds-tag-removable/ds-tag-removable.a11y.test.js +24 -0
- package/src/ds-tag-removable/ds-tag-removable.js +146 -0
- package/src/ds-tag-removable/ds-tag-removable.stories.js +52 -0
- package/src/ds-tag-removable/ds-tag-removable.test.js +46 -0
- package/src/ds-tag-status/ds-tag-status.a11y.test.js +93 -0
- package/src/ds-tag-status/ds-tag-status.js +164 -0
- package/src/ds-tag-status/ds-tag-status.stories.js +200 -0
- package/src/ds-tag-status/ds-tag-status.test.js +140 -0
- package/src/ds-tag-status/index.js +1 -0
- package/src/ds-textarea/ds-textarea-clearable.test.js +89 -0
- package/src/ds-textarea/ds-textarea.a11y.test.js +66 -0
- package/src/ds-textarea/ds-textarea.js +505 -0
- package/src/ds-textarea/ds-textarea.stories.js +335 -0
- package/src/ds-textarea/ds-textarea.test.js +218 -0
- package/src/ds-textarea/index.js +1 -0
- package/src/ds-thumbnail/ds-thumbnail.js +207 -0
- package/src/ds-thumbnail/ds-thumbnail.stories.js +217 -0
- package/src/ds-thumbnail/ds-thumbnail.test.js +220 -0
- package/src/ds-toast/ds-toast-provider.js +110 -0
- package/src/ds-toast/ds-toast.a11y.test.js +34 -0
- package/src/ds-toast/ds-toast.js +243 -0
- package/src/ds-toast/ds-toast.stories.js +143 -0
- package/src/ds-toast/ds-toast.test.js +93 -0
- package/src/ds-toast/index.js +2 -0
- package/src/ds-tooltip/ds-tooltip.a11y.test.js +110 -0
- package/src/ds-tooltip/ds-tooltip.js +217 -0
- package/src/ds-tooltip/ds-tooltip.mdx +75 -0
- package/src/ds-tooltip/ds-tooltip.stories.js +72 -0
- package/src/ds-tooltip/ds-tooltip.test.js +191 -0
- package/src/ds-tooltip/index.js +1 -0
- package/src/ds-tooltip/positioner.js +117 -0
- package/src/index.js +50 -0
- package/src/mixins/field-label.mixin.js +113 -0
- package/src/mixins/field-message.mixin.js +66 -0
- package/src/token-provider/index.js +1 -0
- package/src/token-provider/token-provider.a11y.test.js +44 -0
- package/src/token-provider/token-provider.js +85 -0
- package/src/token-provider/token-provider.stories.js +105 -0
- package/src/token-provider/token-provider.test.js +134 -0
- package/src/utils/number-input.utils.js +42 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import '../ds-icon-button/ds-icon-button.js';
|
|
3
|
+
import '../ds-icon/ds-icon.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Dialog component for the Design System
|
|
7
|
+
*
|
|
8
|
+
* @element ds-dialog
|
|
9
|
+
*
|
|
10
|
+
* @prop {boolean} open - Whether the dialog is open
|
|
11
|
+
* @prop {string} heading - The title of the dialog
|
|
12
|
+
* @prop {boolean} preventClose - If true, clicking backdrop or Esc won't close it
|
|
13
|
+
* @prop {string} size - 'sm' | 'md' | 'lg' | 'fullscreen' (default: 'md')
|
|
14
|
+
*
|
|
15
|
+
* @slot header - Content for the header (overrides default title/close)
|
|
16
|
+
* @slot body - Main content area
|
|
17
|
+
* @slot footer - Footer area (usually for ds-action-bar)
|
|
18
|
+
* @slot - Default slot, rendering into the body area
|
|
19
|
+
*
|
|
20
|
+
* @event ds-close - Fired when the dialog is requested to close
|
|
21
|
+
* @event ds-open - Fired when the dialog opens
|
|
22
|
+
*/
|
|
23
|
+
export class DsDialog extends LitElement {
|
|
24
|
+
static properties = {
|
|
25
|
+
open: { type: Boolean, reflect: true },
|
|
26
|
+
heading: { type: String },
|
|
27
|
+
preventClose: { type: Boolean, attribute: 'prevent-close' },
|
|
28
|
+
size: { type: String, reflect: true },
|
|
29
|
+
modal: { type: Boolean, reflect: true },
|
|
30
|
+
position: { type: String, reflect: true }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
static styles = css`
|
|
34
|
+
:host {
|
|
35
|
+
display: contents;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Native Dialog Reset & Base Styles */
|
|
39
|
+
dialog {
|
|
40
|
+
padding: 0;
|
|
41
|
+
border: none;
|
|
42
|
+
border-radius: var(--ds-radius-container, 0px);
|
|
43
|
+
background: var(--ds-color-bg-surface, #ffffff);
|
|
44
|
+
color: var(--ds-color-text-default);
|
|
45
|
+
|
|
46
|
+
/* Tokens */
|
|
47
|
+
box-shadow: var(--ds-elevation-floating, 0 2px 12px 0 #e6e2dc);
|
|
48
|
+
z-index: var(--ds-zindex-modal, 900);
|
|
49
|
+
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
|
|
52
|
+
/* Flex Column Layout for Sticky Header/Footer */
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-direction: column;
|
|
55
|
+
|
|
56
|
+
/* Height Best Practice:
|
|
57
|
+
Use fit-content so it only takes what it needs,
|
|
58
|
+
up to the max-height constraint.
|
|
59
|
+
*/
|
|
60
|
+
height: fit-content;
|
|
61
|
+
max-height: calc(100vh - 48px); /* Safety margin */
|
|
62
|
+
max-width: 100vw;
|
|
63
|
+
|
|
64
|
+
/* Centering via native behavior */
|
|
65
|
+
margin: auto;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* NON-MODAL (Floating Window) BEHAVIOR
|
|
70
|
+
* Ensure it stays fixed on screen and doesn't block clicks outside.
|
|
71
|
+
*/
|
|
72
|
+
dialog:not(.is-modal) {
|
|
73
|
+
position: fixed;
|
|
74
|
+
pointer-events: none; /* Let clicks pass through the "glass" */
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* POSITIONS for Non-Modal */
|
|
78
|
+
/* Center (Default) */
|
|
79
|
+
:host([position="center"]) dialog:not(.is-modal),
|
|
80
|
+
:host(:not([position])) dialog:not(.is-modal) {
|
|
81
|
+
inset: 0;
|
|
82
|
+
margin: auto;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Top Left */
|
|
86
|
+
:host([position="top-left"]) dialog:not(.is-modal) {
|
|
87
|
+
inset: auto;
|
|
88
|
+
top: 24px;
|
|
89
|
+
left: 24px;
|
|
90
|
+
margin: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Top Right */
|
|
94
|
+
:host([position="top-right"]) dialog:not(.is-modal) {
|
|
95
|
+
inset: auto;
|
|
96
|
+
top: 24px;
|
|
97
|
+
right: 24px;
|
|
98
|
+
margin: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Bottom Left */
|
|
102
|
+
:host([position="bottom-left"]) dialog:not(.is-modal) {
|
|
103
|
+
inset: auto;
|
|
104
|
+
bottom: 24px;
|
|
105
|
+
left: 24px;
|
|
106
|
+
margin: 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Bottom Right */
|
|
110
|
+
:host([position="bottom-right"]) dialog:not(.is-modal) {
|
|
111
|
+
inset: auto;
|
|
112
|
+
bottom: 24px;
|
|
113
|
+
right: 24px;
|
|
114
|
+
margin: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Re-enable pointer events on the dialog content itself */
|
|
118
|
+
dialog:not(.is-modal) > * {
|
|
119
|
+
pointer-events: auto;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Ensure styles apply when polyfilled or if display is overridden */
|
|
123
|
+
dialog:not([open]) {
|
|
124
|
+
display: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Remove default focus ring from the dialog container itself */
|
|
128
|
+
dialog:focus,
|
|
129
|
+
dialog:focus-visible {
|
|
130
|
+
outline: none;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Only show backdrop for modal dialogs */
|
|
134
|
+
dialog.is-modal::backdrop {
|
|
135
|
+
background: var(--ds-color-bg-backdrop, #fbfaf9cc);
|
|
136
|
+
backdrop-filter: blur(2px);
|
|
137
|
+
z-index: var(--ds-zindex-backdrop, 800);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Fallback for browsers/polyfills where ::backdrop might not work as expected or for extra safety */
|
|
141
|
+
dialog:not([open])::backdrop {
|
|
142
|
+
display: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* SIZES
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
/* SM: 300px - 520px */
|
|
150
|
+
:host([size="sm"]) dialog {
|
|
151
|
+
width: 100%;
|
|
152
|
+
min-width: 300px;
|
|
153
|
+
max-width: 520px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* MD: 720px */
|
|
157
|
+
:host([size="md"]) dialog {
|
|
158
|
+
width: 720px;
|
|
159
|
+
max-width: calc(100vw - 32px); /* Safety margin */
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* LG: 1080px */
|
|
163
|
+
:host([size="lg"]) dialog {
|
|
164
|
+
width: 1080px;
|
|
165
|
+
max-width: calc(100vw - 32px); /* Safety margin */
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Fullscreen */
|
|
169
|
+
:host([size="fullscreen"]) dialog {
|
|
170
|
+
width: 100vw;
|
|
171
|
+
height: 100vh;
|
|
172
|
+
max-width: 100%;
|
|
173
|
+
max-height: 100%;
|
|
174
|
+
border-radius: 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/*
|
|
178
|
+
* HEADER
|
|
179
|
+
* Padding: 16px Top, 16px Horizontal (0 Bottom, handled by Body gap)
|
|
180
|
+
* Sticky Top (Flex item doesn't shrink)
|
|
181
|
+
*/
|
|
182
|
+
.header {
|
|
183
|
+
flex-shrink: 0;
|
|
184
|
+
display: flex;
|
|
185
|
+
justify-content: space-between;
|
|
186
|
+
align-items: center;
|
|
187
|
+
padding: 16px 16px 0 16px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.header-title {
|
|
191
|
+
/* Token: title-s -> typo_heading_sm */
|
|
192
|
+
font: var(--ds-typo-heading-sm, 700 18px 'Noto Sans', sans-serif);
|
|
193
|
+
margin: 0;
|
|
194
|
+
color: var(--ds-color-text-default);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/*
|
|
198
|
+
* BODY
|
|
199
|
+
* Scrollable area
|
|
200
|
+
* Padding: 8px Top, 24px Bottom, 16px Horizontal
|
|
201
|
+
* Token: body-default -> typo_content_body_regular
|
|
202
|
+
*/
|
|
203
|
+
.body {
|
|
204
|
+
flex-grow: 1;
|
|
205
|
+
overflow-y: auto;
|
|
206
|
+
padding: 8px 16px 24px 16px;
|
|
207
|
+
font: var(--ds-typo-content-body-regular, 400 14px 'Noto Sans', sans-serif);
|
|
208
|
+
color: var(--ds-color-text-default);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Remove default outline and apply custom one if needed,
|
|
212
|
+
but typically internal content should manage its own focus styles.
|
|
213
|
+
User requested to remove the "native aspect".
|
|
214
|
+
We'll remove the outline from the container and rely on children-level focus
|
|
215
|
+
or specific focus-visible style if strictly required.
|
|
216
|
+
Reverting the previous .body:focus-visible to simple outline: none
|
|
217
|
+
unless User explicitly asks for a specific focus style there again.
|
|
218
|
+
The request was "focus now looks internal and keeps native aspect... remove that border".
|
|
219
|
+
This likely referred to the DIALOG border.
|
|
220
|
+
We will keep the tokenized focus style for the body content purely for accessibility
|
|
221
|
+
but ensure it's clean (no double borders).
|
|
222
|
+
*/
|
|
223
|
+
.body:focus,
|
|
224
|
+
.body:focus-visible {
|
|
225
|
+
outline: none; /* Let inner elements handle focus, or use a very specific subtle style */
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/*
|
|
229
|
+
* FOOTER
|
|
230
|
+
* Sticky Bottom (Flex item doesn't shrink)
|
|
231
|
+
*/
|
|
232
|
+
.footer {
|
|
233
|
+
flex-shrink: 0;
|
|
234
|
+
}
|
|
235
|
+
`;
|
|
236
|
+
|
|
237
|
+
constructor() {
|
|
238
|
+
super();
|
|
239
|
+
this.open = false;
|
|
240
|
+
this.heading = '';
|
|
241
|
+
this.preventClose = false;
|
|
242
|
+
this.size = 'md';
|
|
243
|
+
this.modal = false; // Default to non-modal (safer)
|
|
244
|
+
this.position = 'center';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
disconnectedCallback() {
|
|
248
|
+
super.disconnectedCallback();
|
|
249
|
+
this._unlockScroll();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
updated(changedProps) {
|
|
253
|
+
// If 'open' changed OR 'modal' changed while open
|
|
254
|
+
if (changedProps.has('open') || (this.open && changedProps.has('modal'))) {
|
|
255
|
+
const dialog = this.shadowRoot.querySelector('dialog');
|
|
256
|
+
|
|
257
|
+
// If we are switching modes (modal <-> non-modal) while open, we might need to reset
|
|
258
|
+
if (changedProps.has('modal') && this.open && dialog.open) {
|
|
259
|
+
dialog.close(); // Close first to reset mode
|
|
260
|
+
this._unlockScroll();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (this.open) {
|
|
264
|
+
// Enforce explicit class for CSS styling (backdrop)
|
|
265
|
+
if (this.modal) {
|
|
266
|
+
dialog.classList.add('is-modal');
|
|
267
|
+
} else {
|
|
268
|
+
dialog.classList.remove('is-modal');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!dialog.open) {
|
|
272
|
+
// Modal vs Non-Modal
|
|
273
|
+
if (this.modal) {
|
|
274
|
+
dialog.showModal();
|
|
275
|
+
this._lockScroll();
|
|
276
|
+
} else {
|
|
277
|
+
dialog.show();
|
|
278
|
+
// Ensure scroll is unlocked if we switched to non-modal
|
|
279
|
+
this._unlockScroll();
|
|
280
|
+
}
|
|
281
|
+
// Only dispatch if it was a fresh open, but logic here might dispatch on mode switch.
|
|
282
|
+
// Assuming typical usage doesn't toggle modal prop often while open, it's acceptable.
|
|
283
|
+
if (changedProps.has('open')) {
|
|
284
|
+
this.dispatchEvent(new CustomEvent('ds-open', { bubbles: true, composed: true }));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
if (dialog.open) {
|
|
289
|
+
dialog.close();
|
|
290
|
+
this._unlockScroll();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Helper to get all focusable elements including those inside slots
|
|
297
|
+
_getFocusableElements() {
|
|
298
|
+
const selector = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable], ds-button:not([disabled]), ds-icon-button:not([disabled]), ds-input:not([disabled]), ds-link, ds-textarea, ds-select, ds-checkbox, ds-radio';
|
|
299
|
+
|
|
300
|
+
const getAllFocusables = (parent) => {
|
|
301
|
+
let elements = [];
|
|
302
|
+
|
|
303
|
+
// Check children
|
|
304
|
+
const nodes = parent.querySelectorAll ? parent.querySelectorAll(selector) : [];
|
|
305
|
+
elements = [...nodes];
|
|
306
|
+
|
|
307
|
+
// Check slots
|
|
308
|
+
const slots = parent.querySelectorAll ? parent.querySelectorAll('slot') : [];
|
|
309
|
+
slots.forEach(slot => {
|
|
310
|
+
const assigned = slot.assignedElements({ flatten: true });
|
|
311
|
+
assigned.forEach(node => {
|
|
312
|
+
if (node.matches && node.matches(selector)) {
|
|
313
|
+
elements.push(node);
|
|
314
|
+
}
|
|
315
|
+
// Recursively find focusables inside the slotted element
|
|
316
|
+
if (node.querySelectorAll) {
|
|
317
|
+
elements = [...elements, ...node.querySelectorAll(selector)];
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return elements;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Start from the dialog element in shadow root
|
|
326
|
+
const dialog = this.shadowRoot.querySelector('dialog');
|
|
327
|
+
return getAllFocusables(dialog).filter(el => {
|
|
328
|
+
// Filter out invisible elements if needed, though 'display:none' is usually handled by not being focusable.
|
|
329
|
+
// Native 'inert' or 'hidden' check:
|
|
330
|
+
return el.offsetParent !== null;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
_lockScroll() {
|
|
335
|
+
if (this.modal) {
|
|
336
|
+
document.body.style.overflow = 'hidden';
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_unlockScroll() {
|
|
341
|
+
document.body.style.overflow = '';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Internal method to trigger close event (explicit action)
|
|
345
|
+
_requestClose() {
|
|
346
|
+
// Unlock scroll BEFORE emitting close event so UI feels responsive
|
|
347
|
+
// Actually, better to do it when the prop changes to false, but
|
|
348
|
+
// if the user handles the event by removing the element or setting open=false,
|
|
349
|
+
// the updated() or disconnectedCallback() will handle it.
|
|
350
|
+
// However, to be safe against edge cases (like simple removal):
|
|
351
|
+
|
|
352
|
+
this.dispatchEvent(new CustomEvent('ds-close', { bubbles: true, composed: true }));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
_handleDialogCancel(e) {
|
|
356
|
+
e.preventDefault(); // Prevent native close immediately
|
|
357
|
+
|
|
358
|
+
if (!this.preventClose) {
|
|
359
|
+
this._requestClose();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_handleBackdropClick(e) {
|
|
364
|
+
// Only applies to modal
|
|
365
|
+
if (!this.modal) return;
|
|
366
|
+
|
|
367
|
+
if (e.target.tagName === 'DIALOG') {
|
|
368
|
+
const rect = e.target.getBoundingClientRect();
|
|
369
|
+
const isInDialog = (rect.top <= e.clientY && e.clientY <= rect.top + rect.height &&
|
|
370
|
+
rect.left <= e.clientX && e.clientX <= rect.left + rect.width);
|
|
371
|
+
|
|
372
|
+
if (!isInDialog) {
|
|
373
|
+
if (!this.preventClose) {
|
|
374
|
+
this._requestClose();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
_handleKeyDown(e) {
|
|
381
|
+
if (!this.open) return;
|
|
382
|
+
|
|
383
|
+
// Close on Escape (for non-modal mostly, as native handles modal Esc)
|
|
384
|
+
if (e.key === 'Escape') {
|
|
385
|
+
if (!this.modal && !this.preventClose) {
|
|
386
|
+
e.preventDefault();
|
|
387
|
+
this._requestClose();
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Explicit Tab Trap for Modal
|
|
393
|
+
if (e.key === 'Tab' && this.modal) {
|
|
394
|
+
const focusables = this._getFocusableElements();
|
|
395
|
+
if (focusables.length === 0) return;
|
|
396
|
+
|
|
397
|
+
const first = focusables[0];
|
|
398
|
+
const last = focusables[focusables.length - 1];
|
|
399
|
+
|
|
400
|
+
// Check active element in both contexts
|
|
401
|
+
const isFirstFocused = (document.activeElement === first) || (this.shadowRoot.activeElement === first);
|
|
402
|
+
const isLastFocused = (document.activeElement === last) || (this.shadowRoot.activeElement === last);
|
|
403
|
+
|
|
404
|
+
if (e.shiftKey) { // Backward
|
|
405
|
+
if (isFirstFocused) {
|
|
406
|
+
e.preventDefault();
|
|
407
|
+
last.focus();
|
|
408
|
+
}
|
|
409
|
+
} else { // Forward
|
|
410
|
+
if (isLastFocused) {
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
first.focus();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
render() {
|
|
419
|
+
return html`
|
|
420
|
+
<dialog
|
|
421
|
+
@cancel=${this._handleDialogCancel}
|
|
422
|
+
@click=${this._handleBackdropClick}
|
|
423
|
+
@keydown=${this._handleKeyDown}
|
|
424
|
+
>
|
|
425
|
+
<div class="header">
|
|
426
|
+
<slot name="header">
|
|
427
|
+
<h2 class="header-title">${this.heading}</h2>
|
|
428
|
+
<ds-icon-button
|
|
429
|
+
variant="action"
|
|
430
|
+
size="m"
|
|
431
|
+
icon="close"
|
|
432
|
+
@click=${this._requestClose}
|
|
433
|
+
label="Close dialog"
|
|
434
|
+
></ds-icon-button>
|
|
435
|
+
</slot>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
<div class="body">
|
|
439
|
+
<slot name="body">
|
|
440
|
+
<slot></slot>
|
|
441
|
+
</slot>
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
<div class="footer">
|
|
445
|
+
<slot name="footer" @slotchange=${this._handleFooterSlotChange}></slot>
|
|
446
|
+
</div>
|
|
447
|
+
</dialog>
|
|
448
|
+
`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
_handleFooterSlotChange(e) {
|
|
454
|
+
if (this.size === 'fullscreen') {
|
|
455
|
+
const slot = e.target;
|
|
456
|
+
const nodes = slot.assignedElements({ flatten: true });
|
|
457
|
+
nodes.forEach(node => {
|
|
458
|
+
if (node.tagName.toLowerCase() === 'ds-action-bar') {
|
|
459
|
+
node.setAttribute('variant', 'page');
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
customElements.define('ds-dialog', DsDialog);
|