@descope-ui/descope-address-field 0.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.
@@ -0,0 +1,139 @@
1
+ import { createBaseInputClass } from '@descope-ui/common/base-classes';
2
+ import {
3
+ getComponentName,
4
+ forwardAttrs,
5
+ syncAttrs,
6
+ } from '@descope-ui/common/components-helpers';
7
+ import { connectorMixin } from '@descope-ui/common/components-mixins';
8
+ import { compose } from '@descope-ui/common/utils';
9
+ import { GoogleMapsConnector, RadarConnector } from '../../connectors';
10
+
11
+ export const componentName = getComponentName('address-field-internal');
12
+
13
+ const GOOGLE_MAPS_CONNECTOR_TEMPLATE = 'google-maps-places';
14
+ const RADAR_CONNECTOR_TEMPLATE = 'radar';
15
+
16
+ const CONNECTOR_CLASSES = {
17
+ [GOOGLE_MAPS_CONNECTOR_TEMPLATE]: GoogleMapsConnector,
18
+ [RADAR_CONNECTOR_TEMPLATE]: RadarConnector,
19
+ };
20
+
21
+ const BaseInputClass = createBaseInputClass({
22
+ componentName,
23
+ baseSelector: '',
24
+ });
25
+ const initConnectorAttrs = ['public-api-key'];
26
+ const observedAttrs = [...initConnectorAttrs];
27
+
28
+ class RawAddressFieldInternal extends BaseInputClass {
29
+ static get observedAttributes() {
30
+ return [].concat(BaseInputClass.observedAttributes || [], observedAttrs);
31
+ }
32
+
33
+ get errorMsgValueMissing() {
34
+ return (
35
+ this.getAttribute('data-errormessage-value-missing') ||
36
+ this.defaultErrorMsgValueMissing
37
+ );
38
+ }
39
+
40
+ constructor() {
41
+ super();
42
+
43
+ this.innerHTML = `
44
+ <style>
45
+ :host {
46
+ display: inline-block;
47
+ box-sizing: border-box;
48
+ user-select: none;
49
+ max-width: 100%;
50
+ }
51
+
52
+ :host ::slotted {
53
+ padding: 0;
54
+ }
55
+ </style>
56
+ <div>
57
+ <descope-autocomplete-field></descope-autocomplete-field>
58
+ </div>
59
+ `;
60
+
61
+ this.autocompleteField = this.querySelector('descope-autocomplete-field');
62
+ }
63
+
64
+ get value() {
65
+ return this.autocompleteField.value;
66
+ }
67
+
68
+ set value(val) {
69
+ this.autocompleteField.value = val;
70
+ }
71
+
72
+ focus() {
73
+ this.autocompleteField.focus();
74
+ }
75
+
76
+ init() {
77
+ // This event listener needs to be placed before the super.init() call
78
+ this.addEventListener('focus', (e) => {
79
+ // we want to ignore focus events we are dispatching
80
+ if (e.isTrusted) this.autocompleteField.focus();
81
+ });
82
+
83
+ super.init?.();
84
+ this.initAutocomplete();
85
+ }
86
+
87
+ initAutocomplete() {
88
+ forwardAttrs(this, this.autocompleteField, {
89
+ includeAttrs: [
90
+ 'size',
91
+ 'bordered',
92
+ 'label',
93
+ 'label-type',
94
+ 'placeholder',
95
+ 'disabled',
96
+ 'readonly',
97
+ 'required',
98
+ 'full-width',
99
+ 'helper-text',
100
+ 'error-message',
101
+ 'default-value',
102
+ 'data-errormessage-value-missing',
103
+ 'st-host-direction',
104
+ 'allow-custom-value',
105
+ 'min-search-length',
106
+ 'st-error-message-icon',
107
+ 'st-error-message-icon-size',
108
+ 'st-error-message-icon-padding',
109
+ ],
110
+ });
111
+ // This is required since when we remove the invalid attribute from the autocomplete field,
112
+ // we want to reflect the change in the address field component
113
+ syncAttrs(this, this.autocompleteField, { includeAttrs: ['invalid'] });
114
+
115
+ // Bind the connector fetch results fn to the autocomplete field
116
+ this.autocompleteField.fetchResults = this.fetchConnectorResults.bind(this);
117
+ }
118
+
119
+ attributeChangedCallback(attrName, oldValue, newValue) {
120
+ super.attributeChangedCallback?.(attrName, oldValue, newValue);
121
+
122
+ if (oldValue !== newValue) {
123
+ if (initConnectorAttrs.includes(attrName)) {
124
+ this.initializeConnector();
125
+ }
126
+ }
127
+ }
128
+
129
+ getValidity() {
130
+ if (this.isRequired && !this.value) {
131
+ return { valueMissing: true };
132
+ }
133
+ return { valid: true };
134
+ }
135
+ }
136
+
137
+ export const AddressFieldInternal = compose(
138
+ connectorMixin({ connectorClasses: CONNECTOR_CLASSES }),
139
+ )(RawAddressFieldInternal);
@@ -0,0 +1,3 @@
1
+ import { AddressFieldInternal, componentName } from './AddressFieldInternal';
2
+
3
+ customElements.define(componentName, AddressFieldInternal);
@@ -0,0 +1,9 @@
1
+ import '@vaadin/custom-field';
2
+ import '@descope-ui/descope-autocomplete-field';
3
+ import './descope-address-field-internal';
4
+
5
+ import { componentName, AddressFieldClass } from './AddressFieldClass';
6
+
7
+ customElements.define(componentName, AddressFieldClass);
8
+
9
+ export { AddressFieldClass, componentName };
@@ -0,0 +1,119 @@
1
+ /* global google */
2
+
3
+ import {
4
+ createBaseConnectorClass,
5
+ CONNECTOR_ERRORS,
6
+ } from '@descope-ui/common/base-classes';
7
+ import { initGoogleMapsLoader } from './googleScriptInit';
8
+
9
+ export class GoogleMapsConnector extends createBaseConnectorClass() {
10
+ #autocompleteService = null;
11
+
12
+ #initializationPromise = null;
13
+
14
+ constructor(getAttribute) {
15
+ super(getAttribute);
16
+ // Start initialization but don't block constructor
17
+ this.#initializationPromise = this.#initializeAutocompleteService();
18
+ }
19
+
20
+ // eslint-disable-next-line class-methods-use-this
21
+ getRequiredParams() {
22
+ return ['public-api-key'];
23
+ }
24
+
25
+ // eslint-disable-next-line class-methods-use-this
26
+ getOptionalParams() {
27
+ return [
28
+ {
29
+ key: 'includedPrimaryTypes',
30
+ attribute: 'address-types',
31
+ },
32
+ {
33
+ key: 'language',
34
+ attribute: 'address-language',
35
+ },
36
+ {
37
+ key: 'region',
38
+ attribute: 'address-region',
39
+ },
40
+ ];
41
+ }
42
+
43
+ async #initializeAutocompleteService() {
44
+ const apiKey = this.getAttribute('public-api-key');
45
+ if (this.#autocompleteService || !apiKey) {
46
+ return;
47
+ }
48
+
49
+ try {
50
+ initGoogleMapsLoader(apiKey);
51
+
52
+ const { AutocompleteSuggestion } =
53
+ await google.maps.importLibrary('places');
54
+ this.#autocompleteService = AutocompleteSuggestion;
55
+ } catch (error) {
56
+ // eslint-disable-next-line no-console
57
+ console.error(
58
+ 'Failed to initialize Google Maps Autocomplete service:',
59
+ error,
60
+ );
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ async _fetchResults(query) {
66
+ if (!query?.trim()) {
67
+ return { results: [] };
68
+ }
69
+
70
+ try {
71
+ await this.#initializationPromise;
72
+
73
+ const request = {
74
+ input: query,
75
+ };
76
+
77
+ // Add optional parameters
78
+ this.getOptionalParams().forEach(({ attribute, key, defaultValue }) => {
79
+ const value = this.getAttribute(attribute) || defaultValue;
80
+ if (value) {
81
+ if (key === 'includedPrimaryTypes') {
82
+ request[key] = value
83
+ .split(',')
84
+ .map((type) => type.trim())
85
+ .filter(Boolean);
86
+ } else {
87
+ request[key] = value;
88
+ }
89
+ }
90
+ });
91
+
92
+ const { suggestions } =
93
+ await this.#autocompleteService.fetchAutocompleteSuggestions(request);
94
+ return this.#parseResponse(suggestions);
95
+ } catch (error) {
96
+ // eslint-disable-next-line no-console
97
+ console.error('Google Maps Places Autocomplete failed:', error);
98
+ return { results: [], error: CONNECTOR_ERRORS.FETCH_RESULTS_ERROR };
99
+ }
100
+ }
101
+
102
+ // eslint-disable-next-line class-methods-use-this
103
+ #parseResponse(suggestions) {
104
+ if (!suggestions?.length) {
105
+ return { results: [] };
106
+ }
107
+
108
+ return {
109
+ results: suggestions.map((suggestion) => {
110
+ const prediction = suggestion.placePrediction;
111
+ return {
112
+ id: prediction.placeId,
113
+ label: prediction.text.text,
114
+ value: prediction.text.text,
115
+ };
116
+ }),
117
+ };
118
+ }
119
+ }
@@ -0,0 +1,12 @@
1
+ export const initGoogleMapsLoader = (apiKey) => {
2
+ if (window.google?.maps) {
3
+ return;
4
+ }
5
+
6
+ /* eslint-disable */
7
+ // prettier-ignore
8
+ (g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
9
+ key: apiKey,
10
+ });
11
+ /* eslint-enable */
12
+ };
@@ -0,0 +1,2 @@
1
+ export { GoogleMapsConnector } from './google';
2
+ export { RadarConnector } from './radar';
@@ -0,0 +1,100 @@
1
+ import {
2
+ createBaseConnectorClass,
3
+ CONNECTOR_ERRORS,
4
+ } from '@descope-ui/common/base-classes';
5
+
6
+ const RADAR_AUTOCOMPLETE_URL = 'https://api.radar.io/v1/search/autocomplete';
7
+
8
+ export class RadarConnector extends createBaseConnectorClass() {
9
+ // eslint-disable-next-line class-methods-use-this
10
+ getRequiredParams() {
11
+ return ['public-api-key'];
12
+ }
13
+
14
+ // eslint-disable-next-line class-methods-use-this
15
+ getOptionalParams() {
16
+ return [
17
+ {
18
+ key: 'layers',
19
+ attribute: 'address-types',
20
+ },
21
+ {
22
+ key: 'limit',
23
+ attribute: 'address-limit',
24
+ },
25
+ {
26
+ key: 'lang',
27
+ attribute: 'address-language',
28
+ },
29
+ {
30
+ key: 'countryCode',
31
+ attribute: 'address-region',
32
+ },
33
+ ];
34
+ }
35
+
36
+ async _fetchResults(query) {
37
+ if (!query?.trim()) {
38
+ return { results: [] };
39
+ }
40
+
41
+ const apiKey = this.getAttribute('public-api-key');
42
+ const params = new URLSearchParams({
43
+ query: query.trim(),
44
+ });
45
+
46
+ // Add optional parameters
47
+ this.getOptionalParams().forEach(({ attribute, key, defaultValue }) => {
48
+ const value = this.getAttribute(attribute) || defaultValue;
49
+ if (value) {
50
+ if (key === 'layers' || key === 'countryCode') {
51
+ params.append(
52
+ key,
53
+ value
54
+ .split(',')
55
+ .map((type) => type.trim())
56
+ .filter(Boolean)
57
+ .join(','),
58
+ );
59
+ } else {
60
+ params.append(key, value);
61
+ }
62
+ }
63
+ });
64
+
65
+ try {
66
+ const response = await fetch(
67
+ `${RADAR_AUTOCOMPLETE_URL}?${params.toString()}`,
68
+ {
69
+ headers: {
70
+ Authorization: apiKey,
71
+ },
72
+ },
73
+ );
74
+
75
+ if (!response.ok) {
76
+ throw new Error(`Radar API returned ${response.status}`);
77
+ }
78
+
79
+ const data = await response.json();
80
+ return this.#parseResponse(data);
81
+ } catch (error) {
82
+ // eslint-disable-next-line no-console
83
+ console.error('Radar Places Autocomplete failed:', error);
84
+ return { results: [], error: CONNECTOR_ERRORS.FETCH_RESULTS_ERROR };
85
+ }
86
+ }
87
+
88
+ // eslint-disable-next-line class-methods-use-this
89
+ #parseResponse(data) {
90
+ if (!data?.addresses?.length) {
91
+ return { results: [] };
92
+ }
93
+ return {
94
+ results: data.addresses.map((address) => ({
95
+ label: address.formattedAddress,
96
+ value: address.formattedAddress,
97
+ })),
98
+ };
99
+ }
100
+ }
package/src/theme.js ADDED
@@ -0,0 +1,16 @@
1
+ import { AddressFieldClass } from './component/AddressFieldClass';
2
+ import { refs } from '@descope-ui/theme-input-wrapper';
3
+
4
+ const vars = AddressFieldClass.cssVarList;
5
+
6
+ const addressField = {
7
+ [vars.hostWidth]: refs.width,
8
+ [vars.hostDirection]: refs.direction,
9
+
10
+ _fullWidth: {
11
+ [vars.hostWidth]: '100%',
12
+ },
13
+ };
14
+
15
+ export default addressField;
16
+ export { vars };
@@ -0,0 +1,166 @@
1
+ import { componentName } from '../src/component';
2
+ import { withForm } from '@descope-ui/common/sb-helpers';
3
+ import { createBaseConnectorClass } from '@descope-ui/common/base-classes';
4
+ import {
5
+ labelControl,
6
+ placeholderControl,
7
+ sizeControl,
8
+ fullWidthControl,
9
+ directionControl,
10
+ disabledControl,
11
+ readOnlyControl,
12
+ requiredControl,
13
+ borderedControl,
14
+ errorMissingValueControl,
15
+ inputLabelTypeControl,
16
+ errorMessageIconControl,
17
+ errorMessageIconAttrs,
18
+ } from '@descope-ui/common/sb-controls';
19
+ import { MockConnector, MOCK_CONNECTOR_TEMPLATE } from './mockConnector';
20
+
21
+ const Template = ({
22
+ label,
23
+ placeholder,
24
+ size,
25
+ bordered,
26
+ direction,
27
+ required,
28
+ disabled,
29
+ readonly,
30
+ 'default-value': defaultValue,
31
+ 'full-width': fullWidth,
32
+ 'label-type': labelType,
33
+ 'data-errormessage-value-missing': customErrorMessage,
34
+ 'allow-custom-value': allowCustomValue,
35
+ 'min-search-length': minSearchLength,
36
+ 'connector-template': connectorTemplate,
37
+ 'public-api-key': apiKey,
38
+ 'address-types': addressTypes,
39
+ 'address-language': addressLanguage,
40
+ 'address-region': addressRegion,
41
+ 'address-limit': addressLimit,
42
+ errorMsgIcon,
43
+ }) => {
44
+ const addMockConnectorScript = `
45
+ <script>
46
+ const createBaseConnectorClass = ${createBaseConnectorClass};
47
+ setTimeout(() => {
48
+ const addressField = document.querySelector("descope-address-field");
49
+ if (addressField) {
50
+ addressField.connectorClasses = {
51
+ ...addressField.connectorClasses,
52
+ "${MOCK_CONNECTOR_TEMPLATE}": ${MockConnector}
53
+ };
54
+ addressField.setAttribute("connector-template", "${MOCK_CONNECTOR_TEMPLATE}");
55
+ }
56
+ }, 0);
57
+ </script>`;
58
+
59
+ return withForm(`
60
+ <descope-address-field
61
+ size="${size}"
62
+ bordered="${bordered}"
63
+ label="${label || ''}"
64
+ label-type="${labelType || ''}"
65
+ disabled="${disabled || false}"
66
+ placeholder="${placeholder || ''}"
67
+ readonly="${readonly || false}"
68
+ required="${required || false}"
69
+ full-width="${fullWidth || false}"
70
+ st-host-direction="${direction ?? ''}"
71
+ default-value="${defaultValue || ''}"
72
+ data-errormessage-value-missing="${customErrorMessage || ''}"
73
+ allow-custom-value="${allowCustomValue || false}"
74
+ min-search-length="${minSearchLength || ''}"
75
+ connector-template="${
76
+ connectorTemplate !== MOCK_CONNECTOR_TEMPLATE ? connectorTemplate : ''
77
+ }"
78
+ public-api-key="${apiKey || ''}"
79
+ address-types="${addressTypes || ''}"
80
+ address-language="${addressLanguage || ''}"
81
+ address-region="${addressRegion || ''}"
82
+ address-limit="${addressLimit || ''}"
83
+ ${errorMsgIcon ? errorMessageIconAttrs : ''}
84
+ ></descope-address-field>
85
+ ${connectorTemplate === MOCK_CONNECTOR_TEMPLATE ? addMockConnectorScript : ''}
86
+ `);
87
+ };
88
+
89
+ export default {
90
+ component: componentName,
91
+ title: 'descope-address-field',
92
+ argTypes: {
93
+ ...labelControl,
94
+ ...placeholderControl,
95
+ ...inputLabelTypeControl,
96
+ ...sizeControl,
97
+ ...fullWidthControl,
98
+ ...disabledControl,
99
+ ...readOnlyControl,
100
+ ...requiredControl,
101
+ ...borderedControl,
102
+ ...errorMissingValueControl,
103
+ ...directionControl,
104
+ 'min-search-length': {
105
+ name: 'Min Search Length',
106
+ control: { type: 'number' },
107
+ },
108
+ 'allow-custom-value': {
109
+ name: 'Allow Custom Value',
110
+ control: { type: 'boolean' },
111
+ },
112
+ 'connector-template': {
113
+ name: 'Connector Template',
114
+ control: { type: 'text' },
115
+ },
116
+ 'public-api-key': {
117
+ name: 'API Key',
118
+ control: { type: 'text' },
119
+ },
120
+ 'address-types': {
121
+ name: 'Address Types',
122
+ control: { type: 'text' },
123
+ },
124
+ 'address-language': {
125
+ name: 'Language',
126
+ control: { type: 'text' },
127
+ },
128
+ 'address-region': {
129
+ name: 'Region',
130
+ control: { type: 'text' },
131
+ },
132
+ 'address-limit': {
133
+ name: 'Address Limit',
134
+ control: { type: 'number' },
135
+ },
136
+ ...errorMessageIconControl,
137
+ 'default-value': {
138
+ name: 'Default Value',
139
+ control: { type: 'text' },
140
+ },
141
+ },
142
+ };
143
+
144
+ export const Default = Template.bind({});
145
+
146
+ Default.args = {
147
+ bordered: true,
148
+ size: 'md',
149
+ 'connector-template': MOCK_CONNECTOR_TEMPLATE,
150
+ };
151
+
152
+ export const GoogleMapsPlaces = Template.bind({});
153
+
154
+ GoogleMapsPlaces.args = {
155
+ ...Default.args,
156
+ 'connector-template': 'google-maps-places',
157
+ 'public-api-key': '',
158
+ };
159
+
160
+ export const Radar = Template.bind({});
161
+
162
+ Radar.args = {
163
+ ...Default.args,
164
+ 'connector-template': 'radar',
165
+ 'public-api-key': '',
166
+ };
@@ -0,0 +1,82 @@
1
+ import { createBaseConnectorClass } from '@descope-ui/common/base-classes';
2
+ export const MOCK_CONNECTOR_TEMPLATE = 'mock-connector';
3
+
4
+ export class MockConnector extends createBaseConnectorClass() {
5
+ get mockAddresses() {
6
+ return [
7
+ {
8
+ id: 'mock-1',
9
+ address: '123 Mock Street, Mock City, MC 12345',
10
+ },
11
+ {
12
+ id: 'mock-2',
13
+ address: '456 Test Avenue, Mock Town, TT 67890',
14
+ },
15
+ {
16
+ id: 'mock-3',
17
+ address: '789 Sample Boulevard, Mock City, DC 54321',
18
+ },
19
+ {
20
+ id: 'mock-4',
21
+ address: '321 Example Lane, Mock Valley, TV 98765',
22
+ },
23
+ {
24
+ id: 'mock-5',
25
+ address: '5511 Test Lane, Test Valley, TV 98765',
26
+ },
27
+ ];
28
+ }
29
+
30
+ // eslint-disable-next-line class-methods-use-this
31
+ getRequiredParams() {
32
+ return [];
33
+ }
34
+
35
+ // eslint-disable-next-line class-methods-use-this
36
+ async _fetchResults(query) {
37
+ let lowerQuery = query?.toLowerCase();
38
+ if (!lowerQuery || !lowerQuery.trim()) {
39
+ return {
40
+ results: [],
41
+ };
42
+ }
43
+ return new Promise((resolve, reject) => {
44
+ const results = [...this.mockAddresses];
45
+ // Add dynamic address at the start if query doesn't start with "no"
46
+ if (!lowerQuery.startsWith('no')) {
47
+ results.unshift({
48
+ id: 'mock-dynamic',
49
+ address: `${query} Custom Road, Sample City, SC 54321`,
50
+ });
51
+ }
52
+
53
+ let responseDelay = 100 + Math.random() * 200;
54
+ if (lowerQuery.startsWith('loading')) {
55
+ responseDelay = 5000;
56
+ } else if (lowerQuery.startsWith('fast')) {
57
+ responseDelay = 0;
58
+ lowerQuery = lowerQuery.replace('fast', '');
59
+ }
60
+
61
+ if (lowerQuery.startsWith('error')) {
62
+ setTimeout(() => {
63
+ reject(new Error('something went wrong'));
64
+ }, responseDelay);
65
+ } else {
66
+ setTimeout(() => {
67
+ resolve({
68
+ results: results
69
+ .filter((result) =>
70
+ result.address.toLowerCase().includes(lowerQuery),
71
+ )
72
+ .map((result) => ({
73
+ id: result.id,
74
+ label: result.address,
75
+ value: result.address,
76
+ })),
77
+ });
78
+ }, responseDelay);
79
+ }
80
+ });
81
+ }
82
+ }